// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.data.sources

import android.content.Context
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.OkHttpClient
import org.openapitools.client.infrastructure.ClientException
import org.openapitools.client.infrastructure.ServerException
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.Error
import xyz.apiote.bimba.czwek.api.transitous.api.GeocodeApi
import xyz.apiote.bimba.czwek.api.transitous.api.MapApi
import xyz.apiote.bimba.czwek.api.transitous.model.Match
import xyz.apiote.bimba.czwek.data.ResultFlowItem
import xyz.apiote.bimba.czwek.data.exceptions.TransientException
import xyz.apiote.bimba.czwek.data.traffic.PointOfInterest
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.units.DistanceUnit
import xyz.apiote.bimba.czwek.units.Km
import xyz.apiote.bimba.czwek.units.Metre
import java.util.Locale
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos

class Transitous {
	companion object {
		val MetresPerDegreeLatitude = Metre(111320.0)
	}

	private fun client(context: Context): OkHttpClient {
		return OkHttpClient.Builder()
			.addNetworkInterceptor { chain ->
				chain.proceed(
					chain
						.request()
						.newBuilder()
						.header("User-Agent", context.getString(R.string.applicationId))
						.build()
				)
			}
			.build()
	}


	fun queryPlaces(context: Context, query: String): Flow<ResultFlowItem> = flow {
		try {
			GeocodeApi(client = client(context)).geocode(query).mapNotNull { match ->
				when (match.type) {
					Match.Type.PLACE -> PointOfInterest(
						match.name,
						match.areas.sortedBy { it.adminLevel }.map { it.name }.distinct().joinToString(),
						Position(
							match.lat.toDouble(),
							match.lon.toDouble()
						)
					)

					// TODO to constructor(Match)
					Match.Type.ADDRESS -> PointOfInterest(
						match.name,
						if (match.houseNumber == null && match.street == null) {
							match.areas.sortedBy { it.adminLevel }.map { it.name }.distinct().joinToString()
						} else {
							"${match.houseNumber ?: ""} ${match.street ?: ""}\n${
								match.areas.sortedBy { it.adminLevel }.map { it.name }.distinct().joinToString()
							}"
						},
						Position(
							match.lat.toDouble(),
							match.lon.toDouble()
						)
					)

					Match.Type.STOP -> null
				}
			}.forEach { emit(it) }
		} catch (e: ServerException) {
			emit(
				TransientException(
					e.message,
					Error(e.statusCode, R.string.error_50x, R.drawable.error_server)
				)
			)
		} catch (e: ClientException) {
			emit(
				xyz.apiote.bimba.czwek.data.exceptions.ClientException(
					e.message,
					Error(e.statusCode, R.string.error, R.drawable.error_app)
				)
			)
		}
	}

	fun queryStops(
		context: Context,
		query: String,
		ignoreNotFound: Boolean = false
	): Flow<ResultFlowItem> = flow {
		try {
			GeocodeApi(client = client(context)).geocode(query, Locale.getDefault().language)
				.filter { it.type == Match.Type.STOP }
				.map { Stop(it) }
				.forEach { emit(it) }
		} catch (e: ServerException) {
			if (!(e.statusCode == 404 && ignoreNotFound)) {
				emit(
					TransientException(
						e.message,
						Error(e.statusCode, R.string.error_50x, R.drawable.error_server)
					)
				)
			}
		} catch (e: ClientException) {
			emit(
				xyz.apiote.bimba.czwek.data.exceptions.ClientException(
					e.message,
					Error(e.statusCode, R.string.error, R.drawable.error_app)
				)
			)
		}
	}

	fun locateQueryables(
		context: Context,
		position: Position,
		radius: DistanceUnit = Metre(500.0)
	): Flow<ResultFlowItem> {
		val deltaLatitude = radius.meters() / MetresPerDegreeLatitude.meters()
		val deltaLongitude =
			radius.meters() / (cos(position.positionLatitude * PI / 180) / 360 * 40075000)
		val br = Position(
			position.positionLatitude - deltaLatitude,
			position.positionLongitude + deltaLongitude
		)
		val tl = Position(
			position.positionLatitude + deltaLatitude,
			position.positionLongitude - deltaLongitude
		)
		return locateQueryables(context, br, tl, Stop.distanceComparator(position))
	}

	fun locateQueryables(
		context: Context,
		br: Position,
		tl: Position,
		comparator: Comparator<Stop>? = null
	): Flow<ResultFlowItem> = flow {
		val stops = locateStops(context, br, tl)

		try {
			if (comparator != null) {
				stops.sortedWith(comparator)
			} else {
				stops
			}.forEach {
				emit(it)
			}
		} catch (e: ServerException) {
			emit(
				TransientException(
					e.message,
					Error(e.statusCode, R.string.error_50x, R.drawable.error_server)
				)
			)
		} catch (e: ClientException) {
			emit(
				xyz.apiote.bimba.czwek.data.exceptions.ClientException(
					e.message,
					Error(e.statusCode, R.string.error, R.drawable.error_app)
				)
			)
		}
	}

	fun locateLocatables(
		context: Context,
		br: Position,
		tl: Position
	): Flow<ResultFlowItem> = flow {
		val stops = locateStops(context, br, tl)
		try {
			stops.forEach { emit(it) }
		} catch (e: ServerException) {
			emit(
				TransientException(
					e.message,
					Error(e.statusCode, R.string.error_50x, R.drawable.error_server)
				)
			)
		} catch (e: ClientException) {
			emit(
				xyz.apiote.bimba.czwek.data.exceptions.ClientException(
					e.message,
					Error(e.statusCode, R.string.error, R.drawable.error_app)
				)
			)
		}
	}

	private fun locateStops(
		context: Context,
		br: Position,
		tl: Position
	): List<Stop> {
		val dLat = abs(br.positionLatitude - tl.positionLatitude) / 2
		var dLon = abs(br.positionLongitude - tl.positionLongitude) / 2
		val centre = Position(
			abs(br.positionLatitude + tl.positionLatitude) / 2,
			abs(br.positionLongitude + tl.positionLongitude) / 2
		)

		val latitudeLimit =
			Km(10.0).meters() / MetresPerDegreeLatitude.meters() // ~radius in degrees latitude
		val corners = if (dLat > latitudeLimit) {
			dLon = dLon * latitudeLimit / dLat
			val newBr = Position(centre.positionLatitude - latitudeLimit, centre.positionLongitude + dLon)
			val newTL = Position(centre.positionLatitude + latitudeLimit, centre.positionLongitude - dLon)
			Pair(newBr.toString(), newTL.toString())
		} else {
			Pair(br.toString(), tl.toString())
		}

		return MapApi(client = client(context)).stops(corners.first, corners.second)
			.filter { it.stopId != null }.map { Stop(it) }
	}
}
